12. モザイクプロット

12.1. 概要

モザイクプロット(Mosaic Plot)とは,複数の質的変数に対して,その比率を四角形の面積で表したグラフです. その見た目から**マリメッコプロット(Marimekko Plot)**とも呼ばれます. 分割方法を工夫することで三変数以上にも対応可能ですが,私がよく見るのは二変数に対する描画です.

二変数に対するモザイクプロットは,積上げ棒グラフの棒の太さを,分母の大きさで調整したものと捉えることができます. これにより,二変数を跨いだ(他の棒中の要素との)比較が可能になりますが,目視で面積を測るのは難しい場合があるので,数値を付記すると親切です.

例えば上記は,雑誌別・年代別の作品数の比率を表したモザイクプロットです. 年代ごとの合計作品数に応じて,縦方向の棒の太さが変わっていることがわかります.

12.2. Plotlyによる作図方法

Plotlyで直接モザイクプロットを描画する方法はありません.こちらを参考に,棒グラフを応用して作図します. かなり複雑なので,詳細は以下の作図例にコメントを入れる形で解説します.

12.3. MADB Labを用いた作図例

12.3.1. 下準備

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

import warnings
warnings.filterwarnings('ignore')
# 前処理の結果,以下に分析対象ファイルが格納されていることを想定
PATH_DATA = '../../data/preprocess/out/episodes.csv'
# Jupyter Book用のPlotlyのrenderer
RENDERER = 'plotly_mimetype+notebook'
def add_years_to_df(df, unit_years=10):
    """unit_years単位で区切ったyears列を追加"""
    df_new = df.copy()
    df_new['years'] = \
        pd.to_datetime(df['datePublished']).dt.year \
        // unit_years * unit_years
    df_new['years'] = df_new['years'].astype(str)
    return df_new
def show_fig(fig):
    """Jupyter Bookでも表示可能なようRendererを指定"""
    fig.update_layout(margin=dict(t=50, l=25, r=25, b=25))
    fig.show(renderer=RENDERER)
df = pd.read_csv(PATH_DATA)

12.3.2. 雑誌別・年代別の合計作品数

col_count = 'cname'
# 10年単位で区切ったyearsを追加
df = add_years_to_df(df, 10)
# mcname, yearsで集計
df_plot = \
    df.groupby(['mcname', 'years'])[col_count].\
    nunique().reset_index()
# years単位で集計してdf_plotにカラムを追加
# モザイクプロットの太さを調整するために算出
df_tmp = df_plot.groupby('years')[col_count].sum().reset_index(
    name='years_total')
df_plot = pd.merge(df_plot, df_tmp, how='left', on='years')
# years合計あたりの比率を計算
df_plot['ratio'] = df_plot[col_count] / df_plot['years_total']
# こんな感じでデータになった
df_plot
mcname years cname years_total ratio
0 週刊少年サンデー 1970 399 1593 0.250471
1 週刊少年サンデー 1980 324 1488 0.217742
2 週刊少年サンデー 1990 293 1289 0.227308
3 週刊少年サンデー 2000 309 1490 0.207383
4 週刊少年サンデー 2010 275 1471 0.186948
5 週刊少年ジャンプ 1970 530 1593 0.332706
6 週刊少年ジャンプ 1980 364 1488 0.244624
7 週刊少年ジャンプ 1990 413 1289 0.320403
8 週刊少年ジャンプ 2000 477 1490 0.320134
9 週刊少年ジャンプ 2010 433 1471 0.294358
10 週刊少年チャンピオン 1970 316 1593 0.198368
11 週刊少年チャンピオン 1980 391 1488 0.262769
12 週刊少年チャンピオン 1990 334 1289 0.259116
13 週刊少年チャンピオン 2000 385 1490 0.258389
14 週刊少年チャンピオン 2010 411 1471 0.279402
15 週刊少年マガジン 1970 348 1593 0.218456
16 週刊少年マガジン 1980 409 1488 0.274866
17 週刊少年マガジン 1990 249 1289 0.193173
18 週刊少年マガジン 2000 319 1490 0.214094
19 週刊少年マガジン 2010 352 1471 0.239293
fig = go.Figure()
# mcnameごとにデータを抽出
for mcname in df_plot['mcname'].unique():
    df_tmp = \
        df_plot[df_plot['mcname']==mcname].reset_index(drop=True)
    widths = df_tmp['years_total']
    fig.add_trace(go.Bar(
        name=mcname,
        # x軸の基点を調整
        x=df_tmp['years_total'].cumsum() - widths,
        y=df_tmp['ratio'], text=df_tmp[col_count],
        # 棒の太さを調整
        width=widths,
        offset=0,))
fig.update_xaxes(
    # 目盛りの一を調整
    tickvals=widths.cumsum() - widths/2,
    ticktext=df_plot['years'].unique(),)
fig.update_xaxes(title='期間')
fig.update_yaxes(title='比率')
fig.update_layout(barmode='stack', title_text='雑誌別・年代別の合計作品数')
show_fig(fig)    

12.3.3. 雑誌別・年代別の合計作家数

同じ要領で作家別に集計します.

col_count = 'creator'
# 10年単位で区切ったyearsを追加
df = add_years_to_df(df, 10)
# mcname, yearsで集計
df_plot = \
    df.groupby(['mcname', 'years'])[col_count].\
    nunique().reset_index()
# years単位で集計してdf_plotにカラムを追加
df_tmp = df_plot.groupby('years')[col_count].sum().reset_index(
    name='years_total')
df_plot = pd.merge(df_plot, df_tmp, how='left', on='years')
# years合計あたりの比率を計算
df_plot['ratio'] = df_plot[col_count] / df_plot['years_total']
fig = go.Figure()
for mcname in df_plot['mcname'].unique():
    df_tmp = \
        df_plot[df_plot['mcname']==mcname].reset_index(drop=True)
    widths = df_tmp['years_total']
    fig.add_trace(go.Bar(
        name=mcname,
        x=df_tmp['years_total'].cumsum() - widths,
        y=df_tmp['ratio'], text=df_tmp[col_count],
        width=widths,
        offset=0,))
fig.update_xaxes(
    tickvals=widths.cumsum() - widths/2,
    ticktext=df_plot['years'].unique(),)
fig.update_xaxes(title='期間')
fig.update_yaxes(title='比率')
fig.update_layout(
    barmode='stack', title_text='雑誌別・年代別の合計作家数')
show_fig(fig)